<?php
/** 
 * 
 * PHP Websocket Server using Editor's Draft 17
 * @author G. Peleskei
 *
 */
require_once 'User.class.php';
require_once 'Header.class.php';

class Websocket {
	/**
	 * 
	 * Array of Server-Sockets
	 * @var Array
	 */
	private $servers = array();
	/**
	 * 
	 * Array of Client-Sockets
	 * @var Array
	 */
	private $clients = array();
	/**
	 * 
	 * Array of Users
	 * @var User
	 */
	protected $users = array();
	
	/**
	 * 
	 * Appendix for key verification on handshake
	 * @var String
	 */
	const VERIFY_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
	/**
	 * 
	 * Debug output
	 * @var boolean
	 */
	private $debug = false;
	
	/**
	 * 
	 * Port list for ServerSocket to listen to
	 * @var Array
	 */
	private $ports = array();
	
	/**
	 * 
	 * Using log file output
	 * @var boolean
	 */
	private $logToFile = false;
	/**
	 * 
	 * log file save name
	 * @var String
	 */
	private $logFile = "LogFile.txt";
	
	/**
	 * 
	 * Max. byte length for reading from socket
	 * @var integer
	 */
	private $maxRcvLength = 65536;
	
	/**
	 * 
	 * Constructor:
	 * Saves ports in array,
	 * setting time limit limitless,
	 * setting auto flush output
	 * @param list of ports
	 */
	public function __construct($port = 80 /*, [$port, [$port, [..]]] */) {
		$this->ports[] = $port;
		$ports = func_get_args(); // remaining ports
		for ($i=1; $i<count($ports); $i++) {
			$this->ports[] = $ports[$i];
		}
		set_time_limit(0);
		ob_implicit_flush();
	}
	
	/**
	 * 
	 * creating ServerSockets, binding and listing
	 * @return Websocket
	 */
	public function createServers() {
		foreach ($this->ports as $port) { // open socket for each port
			$server = socket_create_listen($port);
			if ($server === false) {
				$this->log("Failed to open socket on port ".$port);
			} else {
				$this->log("Socket [".$server."] successfully opened on port ".$port);
				$this->servers[] = $server;
			}
		}
		return $this;
	}
	
	/**
	 * 
	 * Starting and running Server in endless loop.
	 */
	public function run() {
		if (count($this->servers) <= 0) die("No servers in list, watch your ports !!");
		$this->console("Server running...");
		$write = null;
		$except = null;
		while(true) {
			$changed = $this->servers;
			socket_select($changed, $write, $except, 0);
			foreach ($changed as $server) {
				// Server code
				$client = socket_accept($server);
				$msg = $this->readFromClient($client);
				// handshake
				$this->console("Handshake with $client...");
				$header = $this->createHeader($msg);
				$response = $this->getResponseHeader($header);
				if ($this->send($response, $client) > 0) {
					$this->clients[] = $client;
					$user = $this->createUser($client, $server, $header);
					$this->users[] = $user;
					$this->console("Handshake successfull...");
					$this->processAfterHandshake($user);
				} else {
					$this->console("Handshake failed...");
				}
			}
			
			if (count($this->clients) <= 0) continue;
			$changed2 = $this->clients;
			socket_select($changed2, $write, $except, 0);
			foreach ($changed2 as $client) {
				// Client code
				// reading message
				$msg = $this->readFromClient($client);
				// is Closing Frame ?
				if ($this->isToClose($msg)) {
					$this->send($this->getCloseFrame(), $client);
					$this->close($client);
				} else { // getting message
					$msg = $this->unMask($msg);
					$this->console("Received msg: $msg");
					$this->process($msg, $client);
				}
			}
		}
	}
	
	/**
	 * 
	 * Process after successfull handshake.
	 * empty, to be filled.
	 * @param User $user
	 * @return Websocket
	 */
	protected function processAfterHandshake($user) {
		// Enter Code here
		
		return $this;
	}
	
	/**
	 * 
	 * send message to all clients.
	 * @param String $msg
	 * @return Websocket
	 */
	protected function sendToAll($msg) {
		foreach ($this->clients as $client) {
			$this->send($msg, $client, true);
		}
		return $this;
	}
	
	/**
	 * 
	 * Close client connection
	 * @param ClientSocket $client
	 * @return Websocket
	 */
	protected function close($client) {
		@socket_close($client);
		try { array_splice($this->clients, @array_search($client, $this->clients), 1); } catch(Exception $e){}
		$user = $this->getUserFromClients($client);
		try { array_splice($this->users, @array_search($user, $this->users), 1); } catch(Exception $e){}
		return $this;
	}
	
	/**
	 * 
	 * Checking if message ist closing frame
	 * @param String $msg
	 * @return boolen true:is closing frame
	 */
	protected function isToClose($msg) {
		$check = (0x1 << 7) | 0x8;
		if (ord($msg[0]) == 0) return true;
		if (strlen($msg) > 0 && ord($msg[0]) == $check) return true;
		return false;
	}
	
	/**
	 * 
	 * Get Server closing frame message
	 * @return String
	 */
	protected function getCloseFrame() {
		return chr((0x1 << 7) | 0x8).chr(0);
	}
	
	/**
	 * 
	 * to process on incoming client message
	 * @param String $msg
	 * @param ClientSocket $client
	 * @return Websocket
	 */
	protected function process($msg, $client) {
		$this->sendToAll($msg);
		return $this;
	}
	
	/**
	 * 
	 * Get User by ClientSocket
	 * @param ClientSocket $client
	 * @return User or null
	 */
	protected function getUserFromClients($client) {
		foreach ($this->users as $user) {
			if ($user->getClient() == $client) return $user;
		}
		return null;
	}
	
	/**
	 * 
	 * Read message from client
	 * @param ClientSocket $client
	 * @return int : length of message
	 * 				 false : connection failed
	 */
	protected function readFromClient($client) {
		@socket_recv($client, $read, $this->maxRcvLength, 0);
		return $read;
	}
	
	/**
	 * 
	 * Send message to client.
	 * @param String $msg
	 * @param ClientSocket $client
	 * @param boolen $encoded :true -> message is encoded
	 * @return int : length of transmitted message
	 * 				 false : connection failed
	 */
	protected function send($msg, $client, $encoded = false) {
		if ($encoded) {
			$msg = $this->encode($msg);
		}
		$write = @socket_write($client, $msg, strlen($msg));
		if($write === false || $write <= 0) {
			$this->close($client);
		}
		return $write;
	}
	
	/**
	 * 
	 * Response header for handshake
	 * @param Header $header
	 * @return String
	 */
	protected function getResponseHeader($header) {
		return 		"HTTP/1.1 101 Switching Protocols\r\n" .
              "Upgrade: websocket\r\n" .
              "Connection: Upgrade\r\n" .
              "Sec-WebSocket-Accept: " . $this->getResponseKey($header->key) . "\r\n" .
			  			"Sec-WebSocket-Version:". $header->version."\r\n" .
			  			"\r\n";
	}
	
	/**
	 * 
	 * Encode message for transfer to client.
	 * @param String $msg
	 * @return Framed String
	 */
	protected function encode($msg) {
		// 0x1 text frame (FIN + opcode)
    $b1 = 0x80 | (0x1 & 0x0f);
    $length = strlen($msg);
 
    if($length <= 125)
      $header = pack('CC', $b1, $length);
    elseif($length > 125 && $length < 65536)
      $header = pack('CCn', $b1, 126, $length);
    elseif($length >= 65536 && $length <= pow(2, 63)) {
    	// some code for 64 bit byte integer
    	$upper = 0;
    	$lower = 0;
    	$this->get64Bit($upper, $lower, $length);
    	$header = pack('CCNN', $b1, 127, $upper, $lower);
    }
    return $header.$msg;
	}
	
	/**
	 * 
	 * get 64 bit representation of integer.
	 * @param integer $upper upper 32 bit of 64 bit
	 * @param integer $lower lower 32 bit of 64 bit
	 * @param integer $value integer to convert
	 * @return Websocket
	 */
	protected final function get64Bit(&$upper, &$lower, $value) {
		$BIGINT_DIVIDER = 0x7fffffff + 1;
		$lower = intval($value % $BIGINT_DIVIDER);
  	$upper = intval(($value - $lower) / $BIGINT_DIVIDER);
  	return $this;
	}
	
	/**
	 * 
	 * Unmask incoming framed message
	 * @param String $msg
	 * @return String Message-Body
	 */
	protected function unMask($msg) {
		$check = (0x1 << 7) | 0x8;
		$length = ord($msg[1]) & 127;
	  if($length == 126) {
			$masks = substr($msg, 4, 4);
			$data = substr($msg, 8);
		} elseif($length == 127) {
			$masks = substr($msg, 10, 4);
			$data = substr($msg, 14);
		} else {
			$masks = substr($msg, 2, 4);
			$data = substr($msg, 6);
		}
						 
		$text = "";
		for ($i = 0; $i < strlen($data); ++$i) {
			$text .= $data[$i] ^ $masks[$i%4];
		}
		return $text;				  
	}
	
	/**
	 * 
	 * Response key for handshake
	 * @param String $key
	 */
	protected function getResponseKey($key) {
		return base64_encode(sha1(trim($key).self::VERIFY_KEY, true));
	}
	
	/**
	 * 
	 * Create header object
	 * @param String $header
	 * @return Header
	 */
	protected function createHeader($header) {
		return new Header($header);
	}
	
	/**
	 * 
	 * create user.
	 * @param ClientSocket $client
	 * @param ServerSocket $server
	 * @param Header $header
	 * @return User
	 */
	protected function createUser($client, $server, $header) {
		return new User($client, $server, $header);
	}

	/**
	 * 
	 * Set Log save file name
	 * @param String $value
	 * @return Websocket
	 */
	public function setLogToFile($value = true) {
		$this->logToFile = $value;
		return $this;
	}
	
	/**
	 * 
	 * Set if log ist put to file
	 * @param $String $value
	 * @return Websocket
	 */
	public function setLogFile($value) {
		$this->logFile = $value;
		return $this;
	}
	
	/**
	 * 
	 * Set max. length for bytes to read from client.
	 * @param integer $value
	 * @return Websocket
	 */
	public function setMaxRcvLength($value) {
		$this->maxRcvLength = $value;
		return $this;
	}
	
	/**
	 * 
	 * Set debug output
	 * @param boolean $value
	 * @return Websocket
	 */
	public function setDebug($value = true) {
		$this->debug = $value;
		return $this;
	}
	
	/**
	 * 
	 * Prints debug
	 * @param String $msg
	 * @return Websocket
	 */
	public function console($msg) {
		if ($this->debug) {
			echo $msg."\n";
		}
		return $this;
	}
	
	/**
	 * 
	 * Logs and prints message
	 * @param message $msg
	 * @return Websocket
	 */
	public function log($msg) {
		if ($this->logToFile) {
			@file_put_contents(date("d.m.Y H:i:s", time()).": ".$msg."\n", $this->logFile);
		}
		echo $msg."\n";
		return $this;
	}
}
?>